package ca.compsci.opent.semantics;

import java.util.List;

import ca.compsci.opent.analysis.DepthFirstAdapter;
import ca.compsci.opent.node.AAndOpr;
import ca.compsci.opent.node.AAssignExpr;
import ca.compsci.opent.node.ABinOprExpr;
import ca.compsci.opent.node.ABooleanExpr;
import ca.compsci.opent.node.AIdentifierExpr;
import ca.compsci.opent.node.AInferDeclStmt;
import ca.compsci.opent.node.AInferInitStmt;
import ca.compsci.opent.node.ANumberExpr;
import ca.compsci.opent.node.AOrOpr;
import ca.compsci.opent.node.AProgram;
import ca.compsci.opent.node.AStringExpr;
import ca.compsci.opent.node.ATypedDeclStmt;
import ca.compsci.opent.node.ATypedInitStmt;
import ca.compsci.opent.node.PExpr;
import ca.compsci.opent.node.POpr;
import ca.compsci.opent.node.TBooleanLit;
import ca.compsci.opent.node.TIdentifier;
import ca.compsci.opent.node.TNumericLit;
import ca.compsci.opent.node.TStringLit;

/* TODO: Implement all semantic analysis here
 *   - check lvalues
 *      - variables are not redeclared in same scope
 *   - check rvalues
 *      - variables visible in scope
 *      - initialization before use
 *   - check types
 *      - assignments are of the right type
 *      - operators have appropriately typed operands
 */

public class SemanticAnalyzer extends DepthFirstAdapter {
	SymbolTable scope = null;
	
	private static final String TYPE_INT32 = "^(int|int32)$";
	private static final String TYPE_STRING  = "^string$";
	private static final String TYPE_BOOLEAN = "^boolean$";
	
	private static final String true_lit = "true";
	private static final String false_lit = "false";
	
	private int line, pos;
	
	@Override
	public void inAProgram(AProgram node) {
		super.inAProgram(node);
		scope = SymbolTable.newScope();
	}
	
	@Override
	public void outAInferDeclStmt(AInferDeclStmt node) {
		super.outAInferDeclStmt(node);
		
		TIdentifier first = node.getIdentifier();
		List<TIdentifier> rest = node.getRest();
		
		// uninitialized
		createVariable(first, false);
		createVariables(rest, false);
	}
	
	@Override
	public void outAInferInitStmt(AInferInitStmt node) {
		super.outAInferInitStmt(node);
		
		PExpr expr = node.getExpr();
		TIdentifier first = node.getIdentifier();
		List<TIdentifier> rest = node.getRest();
		
		// set initialized flag
		createVariable(first, true);
		createVariables(rest, true);
		
		// if expr, then check if type matches (other checks are done in outABinOprExpr();)
		TypedSymbol.Type type = getTypeForExpr(expr);
		checkOrSetVariableType(first, type);
		checkOrSetVariablesType(rest, type);
	}
	
	@Override
	public void outATypedDeclStmt(ATypedDeclStmt node) {
		super.outATypedDeclStmt(node);
		
		TIdentifier first = node.getIdentifier();
		List<TIdentifier> rest = node.getRest();
		
		TIdentifier type_ident = node.getType();
		TypedSymbol.Type type = toType(type_ident);
		
		// uninitialized
		createVariable(first, false);
		createVariables(rest, false);
		
		checkOrSetVariableType(first, type);
		checkOrSetVariablesType(rest, type);
	}
	
	@Override
	public void outATypedInitStmt(ATypedInitStmt node) {
		super.outATypedInitStmt(node);
		
		PExpr expr = node.getExpr();
		TIdentifier first = node.getIdentifier();
		List<TIdentifier> rest = node.getRest();
		
		// set initialized flag
		createVariable(first, true);
		createVariables(rest, true);
		
		TIdentifier type_ident = node.getType();
		TypedSymbol.Type type = toType(type_ident);
		
		checkOrSetVariableType(first, type);
		checkOrSetVariablesType(rest, type);
		
		// if expr, then check if type matches (other checks are done in outABinOprExpr();)
		TypedSymbol.Type expr_type = getTypeForExpr(expr);
		if (!type.equals(expr_type))
			reportAndExit("expected type " + expr_type + " but found " + type, type_ident.getLine(), type_ident.getPos());
	}
	
	@Override
	public void outABinOprExpr(ABinOprExpr node) {
		super.outABinOprExpr(node);
		
		POpr opr = node.getOpr();

		// check for type missmatch
		TypedSymbol.Type left_type = getTypeForExpr(node.getLeft());
		TypedSymbol.Type right_type = getTypeForExpr(node.getRight());
		opr.apply(caseOperationType);
		TypedSymbol.Type expected_type = caseOperationType.result;
		
		if (expected_type.equals(TypedSymbol.Type.Boolean) && !(opr instanceof AAndOpr || opr instanceof AOrOpr))  {
			if (!left_type.equals(right_type))
				reportAndExit("expected both operands of similar type", line, pos);
		} else {
			if (!left_type.equals(expected_type))
				reportAndExit("expected " + expected_type + " but found " + left_type, line, pos);
	
			if (!right_type.equals(expected_type))
				reportAndExit("expected " + expected_type + " but found " + right_type, line, pos);
		}
	}
	
	@Override
	public void outAAssignExpr(AAssignExpr node) {
		super.outAAssignExpr(node);
		
		// check if left_ident is visible
		checkVisibility(node.getIdentifier());
		
		// check if variable is not yet initialized, if so initialize it, else check for type-missmatch
		checkOrSetVariableType(node.getIdentifier(), getTypeForExpr(node.getExpr()));
		initializeVariable(node.getIdentifier());
	}

	
	@Override // R-Value identifiers
	public void outAIdentifierExpr(AIdentifierExpr node) {
		TIdentifier ident = node.getIdentifier();
		
		line = ident.getLine();
		pos  = ident.getPos();
		
		checkVisibility(ident);
		checkInitialized(ident);
	}
	
	@Override
	public void outAStringExpr(AStringExpr node) {
		TStringLit str_lit = node.getStringLit();
		
		line = str_lit.getLine();
		pos  = str_lit.getPos();
		
		String str = str_lit.getText().trim();
		// remove the enclosing double quotes
		String value = str.substring(1, str.length() - 1);
		// parse the escape sequences
		value = value.replaceAll("\\\\\"", "\"")
        			 .replaceAll("\\\\n", "\n")
        			 .replaceAll("\\\\t", "\t")
        			 .replaceAll("\\\\r", "\r")
        			 .replaceAll("\\\\f", "\f")
        			 .replaceAll("\\\\\\\\", "\\");
		scope.add(str, new Literal<String>(TypedSymbol.Type.String, value));
	}
	
	@Override
	public void outANumberExpr(ANumberExpr node) {
		TNumericLit num_lit = node.getNumericLit();
		String value = num_lit.getText();
		
		line = num_lit.getLine();
		pos  = num_lit.getPos();
		
		scope.add(value, new Literal<Integer>(TypedSymbol.Type.Integer, Integer.valueOf(value)));
	}
	
	@Override
	public void outABooleanExpr(ABooleanExpr node) {
		TBooleanLit bool = node.getBooleanLit();
		String value = bool.getText();
		
		line = bool.getLine();
		pos  = bool.getPos();
		
		if (value.equals(true_lit))
			scope.add(value, new Literal<Boolean>(TypedSymbol.Type.Boolean, Boolean.TRUE));
		if (value.equals(false_lit))
			scope.add(value, new Literal<Boolean>(TypedSymbol.Type.Boolean, Boolean.FALSE));
	}

	public SymbolTable getTable() {
		return scope;
	}
	
	// Private Helper methods...
	
	private void checkVisibility(TIdentifier ident) {
		String ident_name = ident.getText().trim();
		if (!scope.isVisible(ident_name))
			reportAndExit(ident_name + " is not in visible scope", ident.getLine(), ident.getPos());
	}
	
	private void createVariable(TIdentifier ident, boolean initialized) {
		String ident_name = ident.getText().trim();
		if (scope.isInScope(ident_name))
			reportAndExit(ident_name + " cannot be redeclared in current scope", ident.getLine(), ident.getPos());
		scope.add(ident_name, new Variable(TypedSymbol.Type.DEFFERED, initialized));
	}
	
	private void createVariables(List<TIdentifier> idents, boolean initialized) {
		for (TIdentifier ident : idents)
			createVariable(ident, initialized);
	}
	
	private void checkInitialized(TIdentifier ident) {
		String var_name = ident.getText().trim();
		Symbol sym = scope.getInfo(var_name);
		if (sym instanceof Variable) {
			Variable var = (Variable) sym;
			if (!var.isInitialized())
				reportAndExit("variable " + var_name + " has no value", ident.getLine(), ident.getPos());
		}
	}
	
	private void initializeVariable(TIdentifier ident) {
		String var_name = ident.getText().trim();
		Variable var = (Variable) scope.getInfo(var_name);
		var.setInitialized(true);
	}
	
	private void checkOrSetVariableType(TIdentifier var_ident, TypedSymbol.Type type) {
		Variable var = (Variable) scope.getInfo(var_ident.getText().trim());
		TypedSymbol.Type var_type = var.getType();
		if (var_type.equals(TypedSymbol.Type.DEFFERED))
			var.setType(type);
		else if (!var_type.equals(type))
			reportAndExit("expected type " + var_type + " but found " + type, var_ident.getLine(), var_ident.getPos());
	}
	
	private void checkOrSetVariablesType(List<TIdentifier> idents, TypedSymbol.Type type) {
		for (TIdentifier ident : idents)
			checkOrSetVariableType(ident, type);
	}
	
	private TypedSymbol.Type getTypeForExpr(PExpr expr) {
		if (expr instanceof ABinOprExpr) {
			ABinOprExpr aboe = (ABinOprExpr) expr;
			aboe.getOpr().apply(caseOperationType);
			return caseOperationType.result;
		}
		if  (expr instanceof AAssignExpr) {
			AAssignExpr aae = (AAssignExpr) expr;
			TypedSymbol var = (TypedSymbol) scope.getInfo(aae.getIdentifier().getText().trim());
			TypedSymbol.Type type = var.getType();
			if (type.equals(TypedSymbol.Type.DEFFERED))
				return getTypeForExpr(aae.getExpr());
			return type;
		}
		if (expr instanceof AIdentifierExpr) {
			AIdentifierExpr aie = (AIdentifierExpr) expr;
			
			// Identifiers from AIdentifierExpr will always be a TypedSymbol
			TypedSymbol.Type type = ((TypedSymbol) scope.getInfo(aie.getIdentifier().getText().trim())).getType();
			if (type.equals(TypedSymbol.Type.DEFFERED))
				return null; // we, technically should never get to this
			else
				return type;
		}
		if (expr instanceof AStringExpr)
			return TypedSymbol.Type.String;
		if (expr instanceof ANumberExpr)
			return TypedSymbol.Type.Integer;
		if (expr instanceof ABooleanExpr)
			return TypedSymbol.Type.Boolean;
		return null;
	}
	
	private TypedSymbol.Type toType(TIdentifier type) {
		String value = type.getText().trim();
		if (value.matches(TYPE_INT32))
			return TypedSymbol.Type.Integer;
		if (value.matches(TYPE_STRING))
			return TypedSymbol.Type.String;
		if (value.matches(TYPE_BOOLEAN))
			return TypedSymbol.Type.Boolean;
		reportAndExit("custom data types are not currently supported!", type.getLine(), type.getPos());
		return null;
	}
	
	private void reportAndExit(String msg, int line, int col) {
		System.err.printf("Semantic error: %s\n", msg);
		if (line != -1) System.err.printf("          line: %d\n", line);
		if (col != -1)  System.err.printf("        column: %d\n", col);
		System.err.println();
		try {
			throw new Exception();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private OperatorTypeAnalyzer caseOperationType = new OperatorTypeAnalyzer(); 
}
